fix(code): ISS-5963 — zero-byte plan.json, absent-workspace fail-open, and a per-command predicate in detect_spurious_complete - #189
Merged
mikeangstadt merged 1 commit intoAug 12, 2026
Conversation
ISS-5963 Layer 1. c3a4305 inverted the early return so a --prd run that claims COMPLETE with no plan.json is flagged PLAN_MISSING_AT_COMPLETION. Three verified gaps remained. - A zero-byte plan.json was still waved through: [[ -f ]] passes on it, jq yields nothing, and the pendingTasks checks then read 0 pending and call the run clean -- so the 0-byte artifact that is the incident's own evidence reported success. classify_plan_artifact now treats absent, zero-byte, and unparseable-JSON alike: no plan was produced. - An absent workspace failed CLOSED. "The artifact was not produced" and "the workspace is gone" are different facts; live-exit and boot-recovery reclaim the workdir right after finalization, so adjudicating a run whose directory has been deleted would flip a genuine success to FAILED with no repair path. Missing workdir now fails open. - The predicate was the --prd proxy, so REQUEST_CHANGES -- whose result bundle also requires plan.json -- was invisible, and EXECUTE was excluded only by the accident of not carrying a PRD. run_owes_plan_json now reads the command: PLAN and REQUEST_CHANGES owe a plan, EXECUTE and the rest are excluded by name, and an unknown command falls back to the --prd proxy so version skew never crashes or blocks in either direction. REQUEST_CHANGES is enforced only on the not-produced-at-all axis. The harness seeds plan.json before an amend, so presence proves nothing there; detecting "the amend produced nothing" needs a pre-run baseline the detector is not given, and that AC is left open rather than guessed. Testing: bash plugins/code/scripts/tests/test_spurious_complete.sh -- 19 pass, 0 fail. Each gap proven counterfactually by reverting its production line: gap 1 leaves 4 red (zero-byte, malformed, whitespace-only, seeded zero-byte all return {}), gap 2 leaves 1 red (deleted workdir reported PLAN_MISSING_AT_COMPLETION), gap 3 leaves 4 red (REQUEST_CHANGES invisible, EXECUTE falsely flagged). uv run pytest plugins/ green; bash -n clean. Risks: Low, and one-sided by design -- every new flag is scoped to a command whose declared bundle requires plan.json, and the two new not-flagged paths (absent workspace, EXECUTE by name) only remove false failures. The pre-existing shellcheck SC1073 in run-loop.sh is unchanged and unrelated.
mikeangstadt
deleted the
fix/iss-5963-spurious-complete-empty-plan-and-fail-open
branch
August 12, 2026 04:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ISS-5963 — Layer 1 of the false-COMPLETED PLAN loop.
detect_spurious_completeinplugins/code/scripts/run-loop.shis the upstream guard for a run that claims COMPLETE without producing the artifact it existed to produce.What
c3a4305already landed (merged in #186, do not re-review): it inverted the early return so a--prdrun with noplan.jsonis flaggedPLAN_MISSING_AT_COMPLETIONinstead of waved through, scoped it byprd_fileso EXECUTE was not swept in, kept theAWAITING_USERhard stop outranking the new branch, and added 6 tests.Three verified gaps remained. This PR closes them.
1. A zero-byte
plan.jsonwas still reported as successThe ticket's own headline evidence is a 0-byte artifact, and that exact input still passed.
[[ -f ]]is true on an empty file, so the run fell through the missing-plan branch entirely;jqthen yielded nothing,pending_countfell back to0, and the detector returned{}— clean. A file that exists is not a file that was produced.classify_plan_artifactnow returnsmissing | empty | unparseable | present, and the first three are all treated as "no plan was written". The emitted subcode staysPLAN_MISSING_AT_COMPLETION(consumers already key off it); only the message distinguishes the three.2. An absent workspace failed CLOSED
With no workdir, the detector previously reported
PLAN_MISSING_AT_COMPLETION. But the artifact was not produced and the workspace no longer exists are different facts, and only the first is evidence of spuriousness. Live-exit and boot-recovery reclaim the temp workdir immediately after finalization, so adjudicating a run whose directory is already gone flips a genuine success to FAILED with no re-run that repairs it — the same BLOCKING issue Layer 2 (#4806, symphony-alpha) hit and fixed by failing open. A missing workdir now means "cannot judge", so it judges nothing (AC3).3.
REQUEST_CHANGESwas not modelled; EXECUTE was excluded only by accidentThe predicate was "was a
--prdpassed?". That is a proxy, and it fails both ways:REQUEST_CHANGES— whose result bundle also declaresplan.jsonrequired — was invisible to the guard, while EXECUTE was excluded only because it happens not to carry a PRD, not because anything said so.run_owes_plan_jsonnow reads the command (CLOSEDLOOP_COMMAND, or the--promptspelling likeexecute-prompt, normalized):plan.json.execution-result.json, written only after a successful commit and push, so a legitimate no-changes run ends without it; a blanket "required artifact missing ⇒ spurious" rule would fail every one of those. There is a fixture for exactly that run (AC4's warning).--prdproxy, i.e. today's behaviour. An older desktop sending nothing and a newer one sending a command this release has never heard of both degrade instead of crashing or blocking (AC5, both directions).Left open, deliberately:
REQUEST_CHANGESis enforced only on the not-produced-at-all axis. The harness seedsplan.jsonbefore an amend, so presence proves nothing there — this PR catches "no plan at all" and "seeded file still zero bytes", but not "the amend ran and produced nothing", which needs a pre-run baseline the detector is not given. That is called out in the code comment rather than guessed at, and the remainder of AC2 is reported open on the ticket.Test plan
bash plugins/code/scripts/tests/test_spurious_complete.sh→ 19 pass, 0 fail (6 pre-existing + 13 new), following the existing file's style.Each gap was proven counterfactually — production line reverted, suite re-run, failure observed, line restored:
classify_plan_artifactreduced to the old[[ -f ]]checkREQUEST_CHANGEScase each returned{}, i.e. reported the run clean[[ ! -d "$workdir" ]]fail-open guard removedPLAN_MISSING_AT_COMPLETION, the genuine-success-to-FAILED fliprun_owes_plan_jsonreduced to[[ -n "$prd_file" ]]REQUEST_CHANGEScases went invisible ({}), and EXECUTE-with-a-PRD plusexecute-promptwere falsely flaggedAlso run:
uv run --group dev ruff check .→ All checks passed.bash -nclean on both changed scripts.uv run --group dev pytest plugins/→ 2103 passed, 3 skipped, exit 0. CI green on all five checks (Plugin Version Bump, Lint, Type Check, Tests, TypeScript (design-inventory)). (shellcheck -S errorreports SC1073/SC1072 onrun-loop.shline ~2222 — verified identical onorigin/main, pre-existing and untouched.)The test file now clears ambient
CLOSEDLOOP_COMMAND/PRD_FILEbefore sourcing, so the two-argument call sites cannot have their per-command branch decided by the shell that happens to be running the suite.Breaking changes
None — additive and internal to the detector. The subcode contract is unchanged. Every newly-flagged case is scoped to a command whose declared bundle requires
plan.json; the two newly un-flagged paths (absent workspace, EXECUTE by name) only remove false failures. Layer 2 in symphony-alpha keeps working against aclaude-pluginswith or without this change — nothing here requires the two to deploy together.Refs ISS-5963. Layer 2 was ISS-5872 / symphony-alpha#4806.